package edu.buaa.energy.server;

import com.alibaba.fastjson.JSON;
import com.google.common.base.Preconditions;
import edu.buaa.common.RuntimeEnv;
import edu.buaa.common.server.DBSocketServer;
import edu.buaa.common.transaction.AbstractTransaction;
import edu.buaa.energy.EnergyDataTxGenerator;
import edu.buaa.energy.transaction.*;
import edu.buaa.utils.Helper;
import edu.buaa.utils.Pair;
import edu.buaa.utils.Triple;
import org.neo4j.graphdb.*;
import org.neo4j.graphdb.schema.IndexDefinition;
import org.neo4j.graphdb.schema.Schema;

import java.io.File;
import java.io.IOException;
import java.util.*;

public class Neo4jServer1 extends DBSocketServer.ReqExecutor {

    private final HashMap<Integer, Long> idMap2Long = new HashMap<>();

    public static void main(String[] args) {
        DBSocketServer server = new DBSocketServer(dbDir(), new Neo4jServer1(), 9833, "true".equals(System.getenv("ALLOW_TX_FAILURE")));
        RuntimeEnv env = RuntimeEnv.getCurrentEnv();
        String serverCodeVersion = env.name() + "." + Helper.codeGitVersion();
        System.out.println("server code version:" + serverCodeVersion);
        try {
            server.start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static File dbDir() {
        String path = Helper.mustEnv("DB_PATH");
        Preconditions.checkNotNull(path, "need arg: DB_PATH");
        File dbDir = new File(path);
        if (!dbDir.exists()) {
            if (dbDir.mkdirs()) return dbDir;
            else throw new IllegalArgumentException("invalid dbDir");
        } else if (!dbDir.isDirectory()) {
            throw new IllegalArgumentException("invalid dbDir");
        }
        return dbDir;
    }

    private enum Edge implements RelationshipType {
        NORMAL, PROP, TIME_LINE, TIME
    }
    
    private Label label(String name){
        return DynamicLabel.label(name);
    }

    @Override
    public void setDB(GraphDatabaseService db) {
        this.db = db;
        try (Transaction tx = db.beginTx()) {
            Schema schema = db.schema();
            boolean hasTimeIndex = false;
            try {
                for(IndexDefinition timeIndex : schema.getIndexes()){
                    hasTimeIndex = true;
                }
                System.out.println("Has the time index already.");
            } catch (IllegalArgumentException e) {
                // There is no this index
            }
            if (!hasTimeIndex) {
                schema.indexFor(label("NODE")).on("id").create();
                System.out.println("Build the NODE index.");
                schema.indexFor(label("Time")).on("st_time").create(); //.on("en_time")
                System.out.println("Build the TIME index.");
            }
            tx.success();
        }
        buildIdMap(db);
    }


    @Override
    protected AbstractTransaction.Result execute(String line) throws RuntimeException {
        AbstractTransaction tx = JSON.parseObject(line, AbstractTransaction.class);
        switch (tx.getTxType()) {
            case tx_import_static_data:
                return execute((ImportStaticDataTx) tx);
            case tx_import_temporal_data:
                return execute((ImportTemporalDataTx) tx);
            case tx_update_temporal_data:
                return execute((UpdateTemporalDataTx) tx);
            case tx_query_snapshot:
                return execute((SnapshotQueryTx) tx);
            case tx_query_snapshot_aggr_max:
                return execute((SnapshotAggrMaxTx) tx);
            case tx_query_snapshot_aggr_duration:
                return execute((SnapshotAggrDurationTx) tx);
            case tx_query_entity_history:
                return execute((EntityHistoryTx) tx);
            case tx_query_road_by_temporal_condition:
                return execute((EntityTemporalConditionTx) tx);
            default:
                throw new UnsupportedOperationException();
        }
    }

    private void buildIdMap(GraphDatabaseService db) {
        try (Transaction tx = db.beginTx();
             ResourceIterator<Node> allDataNodes = db.findNodes(label("NODE"))) {
            while (allDataNodes.hasNext()) {
                Node n = allDataNodes.next();
                Integer idInt = (Integer) n.getProperty("id");
                idMap2Long.put(idInt, n.getId());
            }
            tx.success();
        }
    }

    private AbstractTransaction.Result execute(ImportStaticDataTx tx) {
        Label dataNode = label("NODE");
        try (Transaction t = db.beginTx()) {
            for (ImportStaticDataTx.EnergyNode node : tx.getNodes()) {
                Node n = db.createNode(dataNode);
                n.setProperty("id", node.getRawId());
                n.setProperty("name", node.getName());
                n.setProperty("country", node.getCountry());
                n.setProperty("voltage", node.getVoltage());
                n.setProperty("latitude", node.getLatitude());
                n.setProperty("longitude", node.getLongitude());
                idMap2Long.put(node.getRawId(), n.getId());
            }
            for (ImportStaticDataTx.EnergyRel rel : tx.getRels()) {
                Node s = db.getNodeById(idMap2Long.get(rel.getFromRawId()));
                Node e = db.getNodeById(idMap2Long.get(rel.getToRawId()));
                Relationship r = s.createRelationshipTo(e, Edge.NORMAL);
                r.setProperty("x", rel.getX());
                r.setProperty("y", rel.getY());
                r.setProperty("limit", rel.getLimit());
                r.setProperty("length", rel.getLength());
            }
            t.success();
        }
        return new AbstractTransaction.Result();
    }

    private void setProperties(List<Float> values, Relationship r){
        for(int i = 0; i< EnergyDataTxGenerator.properties.length; i++) {
            String propName = EnergyDataTxGenerator.properties[i];
            Float val = values.get(i);
            r.setProperty(propName, val);
        }
    }

    // Assume that data comes order by time. Thus, multithreading would not work.
    private AbstractTransaction.Result execute(ImportTemporalDataTx tx) {
        try (Transaction ttx = db.beginTx()) {
            for (ImportTemporalDataTx.EnergyStatus s : tx.getData()) {
                Long id = idMap2Long.get(s.getNodeId());
                assert id != null;
                Node n = db.getNodeById(id);
                Node t = getProperTimeNode(s.getTime(), s.getTime()+3599);
                Relationship r = n.createRelationshipTo(t, Edge.TIME);
                setProperties(s.getStatus(), r);
            }
            ttx.success();
        }
        return new AbstractTransaction.Result();
    }

    private Node getProperTimeNode(int startTime, int endTime) {
        Node ret;
        ResourceIterator<Node> nodes = db.findNodes(label("Time"), "st_time", startTime);
        while (nodes.hasNext()) {
            ret = nodes.next();
            if((int)ret.getProperty("en_time")==endTime) return ret;
        }
        ret = db.createNode(label("Time"));
        ret.setProperty("st_time", startTime);
        ret.setProperty("en_time", endTime);
        return ret;
    }

    private void copyProperty(Relationship from, Relationship to){
        for(String propName : EnergyDataTxGenerator.properties) {
            to.setProperty(propName, from.getProperty(propName));
        }
    }

    private AbstractTransaction.Result execute(UpdateTemporalDataTx tx) {
        try (Transaction t = db.beginTx()) {
            Node node = db.findNode(label("NODE"), "id", tx.getEntityId());
            // delete the old node and relationships.
            ArrayList<Relationship> willBeDeleted = new ArrayList<>();
            int mn = Integer.MAX_VALUE, mx = Integer.MIN_VALUE;
            Relationship mnRel = null, mxRel = null;
            for (Relationship r : node.getRelationships(Edge.TIME)) {
                Node timeNode = r.getOtherNode(node);
                int st = (int) timeNode.getProperty("st_time");
                int en = (int) timeNode.getProperty("en_time");
                if (tx.getEndTime() >= st && tx.getStartTime() <= en) {
                    willBeDeleted.add(r);
                    if (st < tx.getStartTime() && tx.getEndTime() < en) {
                        mn = st;
                        mx = en;
                        mnRel = mxRel = r;
                        break;
                    }
                    if (st < tx.getStartTime()) {
                        mnRel = r;
                        mn = st;
                    }
                    if (en > tx.getEndTime()) {
                        mxRel = r;
                        mx = en;
                    }
                }
            }

            // update the first and last range.
            if(mnRel!=null){
                Node t1 = getProperTimeNode(mn, tx.getStartTime() - 1);
                Relationship r = node.createRelationshipTo(t1, Edge.TIME);
                copyProperty(mnRel, r);
            }

            if(mxRel!=null){
                Node t2 = getProperTimeNode(tx.getEndTime() + 1, mx);
                Relationship r = node.createRelationshipTo(t2, Edge.TIME);
                copyProperty(mxRel, r);
            }

            // delete the old value.
            for (Relationship d : willBeDeleted) {
                Node time = d.getOtherNode(node);
                d.delete();
                if (time.getDegree() == 0) time.delete();
            }

            // build the new node.
            Node timeNode = getProperTimeNode(tx.getStartTime(), tx.getEndTime());
            Relationship rel = node.createRelationshipTo(timeNode, Edge.TIME);
            setProperties(tx.getValues(), rel);
            t.success();
        }
        return new AbstractTransaction.Result();
    }

    private AbstractTransaction.Result execute(SnapshotQueryTx tx) {
        try (Transaction t = db.beginTx()) {
            List<Pair<Integer, Float>> res = new ArrayList<>();
            int time = tx.getTimestamp();
            ResourceIterator<Node> timeNodes = db.findNodes(label("Time"));
            ArrayList<Node> cache = new ArrayList<>();
            while (timeNodes.hasNext()) {
                Node node = timeNodes.next();
                int st = (int) node.getProperty("st_time");
                int en = (int) node.getProperty("en_time");
                if (time >= st && time <= en) cache.add(node);
            }
            for (Node tNode : cache) {
                Iterable<Relationship> rels = tNode.getRelationships(Edge.TIME);
                for (Relationship rel : rels) {
                    res.add(Pair.of((Integer) rel.getOtherNode(tNode).getProperty("id"), (float) rel.getProperty(tx.getPropertyName())));
                }
            }
            t.success();
            SnapshotQueryTx.Result result = new SnapshotQueryTx.Result();
            result.setStatus(res);
            return result;
        }
    }

    private AbstractTransaction.Result execute(SnapshotAggrMaxTx tx) {
        try (Transaction t = db.beginTx()) {
            List<Pair<Integer, Float>> res = new ArrayList<>();
            int t0 = tx.getT0(), t1 = tx.getT1();
            ResourceIterator<Node> timeNodes = db.findNodes(label("Time"));
            ArrayList<Node> cache = new ArrayList<>();
            HashMap<Integer, Float> ans = new HashMap<>();
            while (timeNodes.hasNext()) {
                Node node = timeNodes.next();
                int st = (int) node.getProperty("st_time");
                int en = (int) node.getProperty("en_time");
                if (t1 >= st && t0 <= en) {
                    cache.add(node);
                }
            }
            for (Node node : cache) {
                Iterable<Relationship> rels = node.getRelationships(Edge.TIME);
                for (Relationship rel : rels) {
                    ans.merge((Integer) rel.getOtherNode(node).getProperty("id"), (float) rel.getProperty(tx.getP()), Float::max);
                }
            }
            for (Map.Entry<Integer, Float> entry : ans.entrySet()) {
                res.add(Pair.of(entry.getKey(), entry.getValue()));
            }
            t.success();
            SnapshotAggrMaxTx.Result result = new SnapshotAggrMaxTx.Result();
            result.setPropMaxValue(res);
            return result;
        }
    }

    private AbstractTransaction.Result execute(SnapshotAggrDurationTx tx) {
        TreeSet<Float> intStarts = tx.getIntStartTreeSet();
        try (Transaction t = db.beginTx()) {
            List<Triple<Integer, Float, Integer>> res = new ArrayList<>();
            int t0 = tx.getT0(), t1 = tx.getT1();
            ResourceIterator<Node> timeNodes = db.findNodes(label("Time"));
            HashMap<Integer, HashMap<Float, Integer>> ans = new HashMap<>();
            while (timeNodes.hasNext()) {
                Node node = timeNodes.next();
                int st = (int) node.getProperty("st_time");
                int en = (int) node.getProperty("en_time");
                if (t1 >= st && t0 <= en) {
                    int left = Math.max(t0, st);
                    int right = Math.min(t1, en);
                    int duration = right - left + 1;

                    for (Relationship rel : node.getRelationships(Edge.TIME)) {
                        Integer id = (Integer) rel.getOtherNode(node).getProperty("id");
                        float property = (Float) rel.getProperty(tx.getP());
                        Float grp = intStarts.floor(property);
                        if(grp!=null) {
                            HashMap<Float, Integer> tmp = ans.get(id);
                            if (tmp != null) {
                                tmp.merge(grp, duration, Integer::sum);
                            } else {
                                tmp = new HashMap<>();
                                tmp.put(grp, duration);
                            }
                            ans.put(id, tmp);
                        }
                    }
                }
            }
            for (Map.Entry<Integer, HashMap<Float, Integer>> entry : ans.entrySet()) {
                for (Map.Entry<Float, Integer> e : entry.getValue().entrySet()) {
                    res.add(Triple.of(entry.getKey(), e.getKey(), e.getValue()));
                }
            }
            t.success();
            SnapshotAggrDurationTx.Result result = new SnapshotAggrDurationTx.Result();
            result.setStatusDuration(res);
            return result;
        }
    }

    private AbstractTransaction.Result execute(EntityHistoryTx tx) {
        try (Transaction t = db.beginTx()) {
            List<Triple<Integer, Integer, Float>> res = new ArrayList<>();
            int t0 = tx.getBeginTime(), t1 = tx.getEndTime();
            ResourceIterator<Node> timeNodes = db.findNodes(label("Time"));
            while (timeNodes.hasNext()) {
                Node node = timeNodes.next();
                int st = (int) node.getProperty("st_time");
                int en = (int) node.getProperty("en_time");
                if (t1 >= st && t0 <= en) {
                    int left = Math.max(t0, st);
                    int right = Math.min(t1, en);

                    for (Relationship rel : node.getRelationships(Edge.TIME)) {
                        Integer id = (Integer) rel.getOtherNode(node).getProperty("id");
                        if(id==tx.getEntity()){
                            float property = (Float) rel.getProperty(tx.getProp());
                            res.add(Triple.of(left, right, property));
                        }
                    }
                }
            }
            t.success();
            EntityHistoryTx.Result result = new EntityHistoryTx.Result();
            result.setHistory(res);
            return result;
        }
    }

    private AbstractTransaction.Result execute(EntityTemporalConditionTx tx) {
        try (Transaction t = db.beginTx()) {
            int t0 = tx.getT0(), t1 = tx.getT1();
            float vMin = tx.getVMin(), vMax = tx.getVMax();
            ResourceIterator<Node> timeNodes = db.findNodes(label("Time"));
            HashSet<Integer> res = new HashSet<>();
            while (timeNodes.hasNext()) {
                Node node = timeNodes.next();
                int st = (int) node.getProperty("st_time");
                int en = (int) node.getProperty("en_time");
                if (t1 >= st && t0 <= en) {
                    for (Relationship rel : node.getRelationships(Edge.TIME)) {
                        Float property = (Float) rel.getProperty(tx.getP());
                        int node_id = (Integer) rel.getOtherNode(node).getProperty("id");
                        if (property >= vMin && property <= vMax) {
                            res.add(node_id);
                        }
                    }
                }
            }
            t.success();
            EntityTemporalConditionTx.Result result = new EntityTemporalConditionTx.Result();
            result.setEntities(new ArrayList<>(res));
            return result;
        }
    }

}
